Reactivity: 実装してみる
https://ubugeeei.github.io/chibivue/10-minimum-example/030-minimum-reactive.html#これらを踏まえて実装しよう
まずは Dep (targetMapに登録する作用) の定義から
code: packages/reactivity/dep.ts
import { type ReactivityEffect } from './effect' // これから定義
export type Dep = Set<ReactivityEffect>
export const createDep(effect?: ReactivityEffect[]): Dep => {
const dep: Dep = new Set<ReactiveEffect>(effects)
return dep
}
次に targetMap の型定義と ReactiveEffect クラスの作成
code: packages/reactivity/effect.ts
import { Dep, createDep } from '.dep'
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
export let activityEffect: ReactiveEffect | undefined
export class ReactiveEffect<T = any> {
constructor(public fn: () => T) {}
run() {
// ※ fnを実行する前の activeEffect を保持しておいて、実行が終わった後元に戻す。
// これをやらないと、どんどん上書きしてしまって、意図しない挙動をしてしまう
let parent: ReactiveEffect | undefined = activeEffect
activeEffect = this
const res = this.fn()
activeEffect = parent
return res
}
}
export function track(target: object, key: unknown) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map)) // ないなら新しく登録
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()) // ないなら新しく登録
}
if (activeEffect) {
dep.add(activeEffect) // activeEffect を targetMap に登録
}
}
export function trigger(target: object, key?: unknown) {
const depsMap = targetMap.get(target)
if (!depsMap) return // ないなら何もしない
const dep = depsMap.get(key)
if (dep) {
const effects = ...dep
// 登録してある関数をまとめて実行
for (const effect of effects) {
effect.run()
}
}
// ないなら何もしない
}
続いて reactive proxy のハンドラを定義
code: packages/reactivity/baseHandler.ts
import { track, trigger } from './effect'
import { reactive } from './reacitve'
export const mutableHanlders: ProxyHandler<object> = {
get(target: object, key: string | symbol, receiver: object) {
track(target, key)
// Reflect: Proxyが問題なく作用するために使う。receiverの登録が鍵
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect
// NOTE: receiver って何だ?
const res = Reflect.get(target, key, receiver)
// NOTE: objectの場合はreactive関数の結果を返している?
if(res !== null && typeof res = 'object') {
return reactive(res)
}
return res
},
set(target: object, key: string | symbol, value: unknown, receiver: object) {
let oldvalue = (target as any)key
Reflect.set(target, key, value, receiver)
if (hasChanged(value, oldvalue) {
trigger(target, key)
}
return true
}
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
このハンドラを使ってreactive関数を作成する
code: packages/reactivity/reactive.ts
import { mutableHandlers } from './baseHandler'
export function reactive<T extends object>(target: T): T {
const proxy = new Proxy(target, mutableHandlers)
return proxy as T
}
これでリファクティブの実装は完了したので、mountする際に実際にこれを使うようにする
code: packages/runtime-core/apiCreateApp.ts
import { ReactiveEffect } from '../reactivity'
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
): CreateAppFunction<HostElement> {
return function createApp(rootComponent) {
const app: App = {
mount(rootContainer: HostElement) {
const componentRender = rootComponent.setup!()
const updateComponent = () => {
const vnode = componentRender()
render(vnode, rootContainer)
}
// ここから
const effect = new ReactiveEffect(updateComponent)
effect.run()
// ここまで
},
}
return app
}
}
Playgroundで試してみる!
code: examples/playground/main.ts
import { createApp, h, reactive } from 'chibivue'
const app = createApp({
setup() {
const state = reactive({ count: 0 })
const increment = () => {
state.count++
}
return function render() {
return h('div', { id: 'my-app' }, [
h('p', {}, [count: ${state.count}]),
h('button', { onClick: increment }, 'increment'),
])
}
},
})
app.mount('#app')
ただ、この状態だと updateComponentが走るたびに、毎回要素を作ってしまうことになる
一時的な対応として、レンダリング時に要素を全て消してあげる
code: packages/rutime-core/renderer.ts
const render: RootRenderFunction = (vnode, container) => {
while (container.firstChild) container.removeChild(container.firstChild) // 全消し処理を追加
const el = renderVNode(vnode)
hostInsert(el, container)
}